package org.jmc.refmanager.domain.dao.jpa;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Root;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Projections;
import org.jmc.refmanager.domain.dao.GenericDao;
import org.jmc.refmanager.domain.dao.QueryParam;
import org.jmc.refmanager.domain.dao.exception.TechnicalException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Repository
@Transactional(propagation = Propagation.SUPPORTS)
/**
 * Note that these JPA DAO are not transactional.
 * Transaction is supported, but only Services 
 * contain required transactions.
 */
public abstract class GenericDaoJpa<E, PK extends Serializable> extends JpaTemplate
		implements GenericDao<E, PK> {

	protected final Logger mLogger = LoggerFactory.getLogger(this.getClass());

	protected Class<E> persistentClass;

	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
	protected EntityManager em;

	public GenericDaoJpa(Class<E> persistentClass) {
		this.persistentClass = persistentClass;
	}

	public void setEntityManager(EntityManager entityManager) {
		this.em = entityManager;
	}

	public EntityManager getEntityManager() {
		return em;
	}

	/* (non-Javadoc)
	 * @see org.jmc.refmanager.domain.dao.GenericDao#countAll()
	 */
	@Override
	public Integer countAll() {
		StringBuilder vBuilder = new StringBuilder();
		vBuilder.append("select count(o) from ");
		vBuilder.append(this.persistentClass.getName());
		vBuilder.append(" o");
		return ((Long) getEntityManager().createQuery(vBuilder.toString()).getSingleResult()).intValue();
	}

	/* (non-Javadoc)
	 * @see org.jmc.refmanager.domain.dao.GenericDao#count(java.lang.String)
	 */
	@Override
	public Integer count(Query query) {
		return ((Long) query.getSingleResult()).intValue();
	}

	/* (non-Javadoc)
	 * @see org.jmc.refmanager.domain.dao.GenericDao#count(java.lang.String)
	 */
	@Override
	public Integer count(Query query, Object... params) {
		if (params != null) {
			for (int t = 0; t < params.length; t++) {
				query.setParameter(t+1, params[t]);
			}
		}
		return ((Long) query.getSingleResult()).intValue();
	}

	public E find(PK pPK) throws TechnicalException {
		E vResult = null;
		try {
			vResult = find(this.persistentClass, pPK);
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		return vResult;
	}

	@SuppressWarnings("unchecked")
	public List<E> findAll() throws TechnicalException {
		List<E> vResult = null;
		try {
			StringBuilder vBuilder = new StringBuilder();
			vBuilder.append("from ");
			vBuilder.append(this.persistentClass.getName());
			vResult = find(vBuilder.toString());
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		return vResult;
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<E> findAndPaging(final QueryParam<E> queryParam, final Query query) {
		List<E> vResult = null;
		
		try {
			vResult = executeFind(new PagedResultsJpaCallback<E>(query, queryParam.getFirst(), queryParam.getCount()));
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		
		return vResult;
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<E> findAndPaging(final QueryParam<E> queryParam, final Query query, final Object... params) {
		List<E> vResult = null;
		
		try {
			vResult = executeFind(new PagedResultsJpaCallback<E>(query, queryParam.getFirst(), queryParam.getCount(), params));
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		
		return vResult;
	}


	/* (non-Javadoc)
	 * @see org.jmc.refmanager.domain.dao.GenericDao#count(java.lang.Object)
	 */
	@Override
	public Integer count(final QueryParam<E> queryParam) {
		E fromExample = queryParam.getCriteriaExample();
		
		// Query By Example ne fonctionne qu'avec Hibernate => adherence ici au moteur de persistence
		Criteria criteria = ((Session) em.getDelegate()).createCriteria(GenericDaoJpa.this.persistentClass);

		if (fromExample != null) {
			Example example = Example.create(fromExample);
			example.enableLike(MatchMode.START).excludeZeroes();
			
			String[] excludeProperty = queryParam.getExcludeProperties();
			if (excludeProperty != null) {
		        for (String exclude : excludeProperty) {
		            example.excludeProperty(exclude);
		        }
	        }
			criteria.add(example);
			criteria.setProjection(Projections.rowCount());
        }

		return ((Long) criteria.uniqueResult()).intValue();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findAndPaging(final QueryParam<E> queryParam) {
		List<E> vResult = null;
		final E fromExample = queryParam.getCriteriaExample();
		
		vResult = executeFind(new JpaCallback<List<E>>() {
			@Override
			public List<E> doInJpa(EntityManager em) throws PersistenceException {
				// Query By Example ne fonctionne qu'avec Hibernate => adherence ici au moteur de persistence
				Criteria criteria = ((Session) em.getDelegate()).createCriteria(GenericDaoJpa.this.persistentClass);

				if (fromExample != null) {
					Example example = Example.create(fromExample);
					example.enableLike(MatchMode.START).excludeZeroes();
					
					String[] excludeProperty = queryParam.getExcludeProperties();
					if (excludeProperty != null) {
				        for (String exclude : excludeProperty) {
				            example.excludeProperty(exclude);
				        }
			        }
					criteria.add(example);
		        }

				if (queryParam.hasSort()) {
					boolean isAscending = queryParam.isSortAsc();
					String propertyName = queryParam.getSortPropertyName();
					if (isAscending) {
						criteria.addOrder(org.hibernate.criterion.Order.asc(propertyName));
					} else {
						criteria.addOrder(org.hibernate.criterion.Order.desc(propertyName));
					}
				}
				
				//query.setHint("org.hibernate.cacheable", true);
				int first = queryParam.getFirst();
				int max = queryParam.getCount();
				if (first != max && max > 0) {
					criteria.setFirstResult(first);
					criteria.setMaxResults(max);
				}
                
                return criteria.list();
			}
		});

		return vResult;
	}
	
	/**
	 * 
	 * @param startPage
	 * @param maxPerPage
	 * @return
	 */
	@Override
	@SuppressWarnings("unchecked")
	public List<E> findAllAndPaging(final QueryParam<E> queryParam) {
		List<E> vResult = null;
		
		try {
			CriteriaBuilder builder = em.getCriteriaBuilder();
			final CriteriaQuery<E> criteriaQuery = builder.createQuery(this.persistentClass);
			
			if (queryParam.hasSort()) {
				Root<E> entity = criteriaQuery.from(this.persistentClass);
	
				List<Order> orders = criteriaQuery.getOrderList();
				List<Order> newOrders = new ArrayList<Order>(orders);
				if (queryParam.isSortAsc()) {
					newOrders.add(builder.asc(entity.get(queryParam.getSortPropertyName())));
				} else {
					newOrders.add(builder.desc(entity.get(queryParam.getSortPropertyName())));
				}
				criteriaQuery.orderBy(newOrders);
			}
			
			vResult = executeFind(new JpaCallback<List<E>>() {
				@Override
				public List<E> doInJpa(EntityManager em) throws PersistenceException {
					//final Query query = em.createQuery(vBuilder.toString());
					final Query query = em.createQuery(criteriaQuery);
					//query.setHint("org.hibernate.cacheable", true);
                    query.setFirstResult(queryParam.getFirst());
                    query.setMaxResults(queryParam.getCount());
                    
                    return query.getResultList();
				}
			});
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		
		return vResult;
	}
	
	/**
	 * 
	 */
	public void remove(PK pPK) throws TechnicalException {
		try {
			E vEntity = find(pPK);
			remove(vEntity);
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
	}

	public E save(E pEntity) throws TechnicalException {
		E vResult = null;
		try {
			vResult = merge(pEntity);
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		return vResult;
	}

	public boolean exists(PK pPK) throws TechnicalException {
		boolean vResult = false;
		try {
			if (find(pPK) != null) {
				vResult = true;
			}
		} catch (PersistenceException e) {
			TechnicalException vException = new TechnicalException();
			vException.initCause(e);
			throw vException;
		}
		return vResult;
	}

	/* (non-Javadoc)
	 * @see org.jmc.refmanager.domain.dao.GenericDao#createQuery(java.lang.String)
	 */
	@Override
	public Query createQuery(String jpql) {
		return em.createQuery(jpql);
	}

	/* (non-Javadoc)
	 * @see org.jmc.refmanager.domain.dao.GenericDao#createNamedQuery(java.lang.String)
	 */
	@Override
	public Query createNamedQuery(String queryName) {
		return em.createNamedQuery(queryName);
	}
}
